/* Copyright (c) 2009 Google Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.jessies.blogbaboon;

import com.apple.eawt.*;
import com.google.gdata.client.*;
import com.google.gdata.client.blogger.*;
import com.google.gdata.data.*;
import com.google.gdata.data.blogger.*;
import com.google.gdata.util.*;
import e.forms.*;
import e.gui.*;
import e.util.*;
import java.awt.*;
import java.awt.event.*;
import java.io.*;
import java.net.*;
import java.util.*;
import java.util.List;
import javax.swing.*;
import javax.swing.event.*;
import org.jdesktop.swingworker.SwingWorker;

public class BlogBaboon extends MainFrame {
  private static final String FEED_URI_BASE = "http://www.blogger.com/feeds";
  private static final String METAFEED_URL = FEED_URI_BASE + "/default/blogs";
  
  private Preferences preferences;
  
  private BloggerService bloggerService;
  private BlogInfo chosenBlog;
  
  private JTextField usernameField = new JTextField(40);
  private JPasswordField passwordField = new JPasswordField(40);
  
  private final PostTableModel postTableModel = new PostTableModel();
  private final JTable postTable = new ETable(postTableModel);
  
  public final Action editPostAction;
  public final Action deletePostAction;
  public final Action newPostAction;
  public final FocusListener actionStateUpdater = new FocusListener() {
    public void focusGained(FocusEvent e) {
      updateActionEnabledState();
    }
    public void focusLost(FocusEvent e) {
      updateActionEnabledState();
    }
  };
  
  public static void main(String[] args) {
    EventQueue.invokeLater(new Runnable() {
      public void run() {
        GuiUtilities.initLookAndFeel();
        new BlogBaboon();
      }
    });
  }
  
  private BlogBaboon() {
    super("BlogBaboon");
    
    initPreferences();
    
    final String ORG_ID = "org.jessies";
    final String APP_ID = "BlogBaboon";
    final String APP_VERSION = "2";
    this.bloggerService = new BloggerService(ORG_ID + "-" + APP_ID + "-" + APP_VERSION);
    
    this.editPostAction = new EditPostAction(this);
    this.deletePostAction = new DeletePostAction(this);
    this.newPostAction = new NewPostAction(this);
    editPostAction.setEnabled(false);
    deletePostAction.setEnabled(false);
    newPostAction.setEnabled(false);
    
    setSize(new Dimension(640, 480));
    setJMenuBar(new BlogBaboonMenuBar(this, null));
    
    setContentPane(makeLoginUi());
    
    initAboutBox();
    initMacOsEventHandlers();
    
    setVisible(true);
    
    // Do we have a default username?
    final String defaultUsername = preferences.getString(BlogBaboonPreferences.DEFAULT_USERNAME);
    if (defaultUsername.length() > 0) {
      usernameField.setText(defaultUsername);
      EventQueue.invokeLater(new Runnable() {
        public void run() {
          passwordField.requestFocusInWindow();
        }
      });
    }
  }
  
  private void initPreferences() {
    this.preferences = new BlogBaboonPreferences();
    preferences.readFromDisk();
    preferences.addPreferencesListener(new Preferences.Listener() {
      public void preferencesChanged() {
        for (EditingWindow editingWindow : InstanceTracker.getInstancesOfClass(EditingWindow.class)) {
          editingWindow.preferencesChanged();
        }
      }
    });
  }
  
  public boolean willQuit() {
    for (EditingWindow editingWindow : InstanceTracker.getInstancesOfClass(EditingWindow.class)) {
      if (!editingWindow.maybeClose()) {
        return false;
      }
    }
    return true;
  }
  
  public Preferences getPreferences() {
    return preferences;
  }
  
  private JComponent makeLoginUi() {
    final ELabel statusLabel = new ELabel();
    
    final JLabel label = makeBoldLabel("Sign in to blogger.com!");
    try {
      final Icon bloggerIcon = new ImageIcon(new URL("http://www.google.com/options/icons/blogger.gif"));
      label.setIcon(bloggerIcon);
    } catch (Exception ex) {
      Log.warn("Couldn't get blogger icon.", ex);
    }
    
    FormPanel loginForm = new FormPanel();
    loginForm.setBackground(Color.WHITE);
    loginForm.addRow("", label);
    loginForm.addRow("Username:", usernameField);
    loginForm.addRow("Password:", passwordField);
    loginForm.addRow("", statusLabel);
    
    // Hitting Enter in the username field takes focus to the password field...
    usernameField.addActionListener(new ActionListener() {
      // Java 6: @Override
      public void actionPerformed(ActionEvent e) {
        passwordField.requestFocusInWindow();
      }
    });
    
    // Hitting Enter in the password field attempts to log in...
    passwordField.addActionListener(new ActionListener() {
      public void actionPerformed(ActionEvent e) {
        usernameField.setEnabled(false);
        passwordField.setEnabled(false);
        new LoginWorker(statusLabel).execute();
      }
    });
    
    return loginForm;
  }
  
  private JLabel makeBoldLabel(String text) {
    final JLabel result = new JLabel(text);
    result.setFont(result.getFont().deriveFont(Font.BOLD));
    return result;
  }
  
  private class LoginWorker extends SwingWorker<List<BlogInfo>, String> {
    private JLabel statusLabel;
    
    private String password;
    
    public LoginWorker(JLabel label) {
      this.statusLabel = label;
      this.password = new String(passwordField.getPassword());
    }
    
    // Java 6: @Override
    public void actionPerformed(ActionEvent e) {
    }
    
    @Override public List<BlogInfo> doInBackground() throws ServiceException, IOException {
      // Authenticate using ClientLogin (username/password).
      // FIXME: if we get a CAPTCHA failure, send the user to
      // https://www.google.com/accounts/DisplayUnlockCaptcha?service=blogger
      // and offer a "try again" button for when they come back.
      publish("Logging in...");
      bloggerService.setUserCredentials(usernameField.getText(), password);
      publish("Retrieving list of blogs...");
      return retrieveBlogList();
    }
    
    // Lets you use "publish" to update the status text safely.
    @Override protected void process(List<String> statuses) {
      statusLabel.setText(statuses.get(statuses.size() - 1));
    }
    
    @Override protected void done() {
      try {
        List<BlogInfo> blogs = get();
        if (blogs.size() == 0) {
          // FIXME: offer a link to "the web interface".
          SimpleDialog.showAlert(BlogBaboon.this, "Couldn't find a blog!", "According to the server, you have an account, but you don't have a blog. Please create a blog via the web interface.");
        } else if (blogs.size() == 1) {
          blogChosen(blogs.get(0), statusLabel);
        } else {
          switchToBlogChoiceUi(blogs);
        }
      } catch (Exception ex) {
        // FIXME: we might have logged in okay and experienced trouble collecting the blog list.
        Log.warn("Login failed (internal error).", ex);
        // FIXME: some user-visible indication of what the failure was!
        statusLabel.setText("");
        
        passwordField.setEnabled(true);
        passwordField.setText("");
        usernameField.setEnabled(true);
        usernameField.requestFocusInWindow();
        usernameField.selectAll();
      }
    }
  }
  
  
  private void blogChosen(BlogInfo blogInfo, JLabel statusLabel) {
    this.chosenBlog = blogInfo;
    new BlogChosenWorker(blogInfo, statusLabel).execute();
  }
  
  private void switchToBlogChoiceUi(List<BlogInfo> blogs) {
    final ELabel statusLabel = new ELabel();
    
    final JList blogList = new JList(blogs.toArray());
    // FIXME: custom cell renderer extending JPanel, with two JLabels, to show title and summary?
    
    final JButton continueButton = new JButton("Continue");
    continueButton.setOpaque(false);
    getRootPane().setDefaultButton(continueButton);
    
    final JPanel buttonPanel = new JPanel(new BorderLayout());
    buttonPanel.setBorder(BorderFactory.createEmptyBorder(0, 0, 12, 0));
    buttonPanel.setOpaque(false);
    buttonPanel.add(statusLabel, BorderLayout.WEST);
    buttonPanel.add(continueButton, BorderLayout.EAST);
    
    FormPanel blogChoiceForm = new FormPanel();
    blogChoiceForm.setBackground(Color.WHITE);
    blogChoiceForm.addRow("", makeBoldLabel("Choose which blog to work on..."));
    blogChoiceForm.addRow("Blog:", new JScrollPane(blogList));
    blogChoiceForm.addRow("", buttonPanel);
    
    final ActionListener continueListener = new ActionListener() {
      public void actionPerformed(ActionEvent e) {
        BlogInfo blogInfo = (BlogInfo) blogList.getSelectedValue();
        if (blogInfo != null) {
          blogList.setEnabled(false);
          continueButton.setEnabled(false);
          getRootPane().setDefaultButton(null);
          
          blogChosen(blogInfo, statusLabel);
        }
      }
    };
    
    ComponentUtilities.bindDoubleClickAndEnter(blogList, continueListener);
    continueButton.addActionListener(continueListener);
    
    setContentPane(blogChoiceForm);
    invalidate();
    validate();
    
    blogList.requestFocusInWindow();
    blogList.setSelectedIndex(0);
  }
  
  private class BlogChosenWorker extends SwingWorker<List<PostEntry>, String> {
    private BlogInfo blog;
    private JLabel statusLabel;
    
    public BlogChosenWorker(BlogInfo blog, JLabel statusLabel) {
      this.blog = blog;
      this.statusLabel = statusLabel;
    }
    
    @Override public List<PostEntry> doInBackground() throws ServiceException, IOException {
      publish("Retrieving list of posts...");
      return retrievePostList();
    }
    
    // Lets you use "publish" to update the status text safely.
    @Override protected void process(List<String> statuses) {
      statusLabel.setText(statuses.get(statuses.size() - 1));
    }
    
    @Override protected void done() {
      try {
        switchToPostUi(get());
      } catch (Exception ex) {
        // FIXME: we might have logged in okay and experienced trouble collecting the blog list.
        Log.warn("Post retrieval failed.", ex);
        // FIXME: some user-visible indication of what the failure was!
        statusLabel.setText("");
      }
    }
  }
  
  public PostEntry getSelectedPost() {
    final int row = postTable.getSelectedRow();
    return (row != -1) ? postTableModel.getPost(row) : null;
  }
  
  private static void fixColumnWidth(JTable table, int columnIndex, Object sampleValue) {
    // Get width of sample column data.
    final javax.swing.table.TableCellRenderer renderer = table.getCellRenderer(0, columnIndex);
    final Component component = renderer.getTableCellRendererComponent(table, sampleValue, false, false, 0, columnIndex);
    final int width = component.getPreferredSize().width + 4;
    
    // Set the column's width to correspond.
    javax.swing.table.TableColumn column = table.getColumnModel().getColumn(columnIndex);
    column.setPreferredWidth(width);
    column.setMinWidth(width);
    column.setMaxWidth(width);
  }
  
  public void updateActionEnabledState() {
    final Component focusOwner = KeyboardFocusManager.getCurrentKeyboardFocusManager().getPermanentFocusOwner();
    final boolean hasSelection = (focusOwner == postTable) && (getSelectedPost() != null);
    deletePostAction.setEnabled(hasSelection);
    editPostAction.setEnabled(hasSelection);
  }
  
  private void switchToPostUi(List<PostEntry> blogPosts) {
    newPostAction.setEnabled(true);
    
    postTableModel.setPosts(blogPosts);
    
    postTable.getSelectionModel().addListSelectionListener(new ListSelectionListener() {
      public void valueChanged(ListSelectionEvent e) {
        updateActionEnabledState();
      }
    });
    postTable.addFocusListener(actionStateUpdater);
    
    postTable.setAutoResizeMode(JTable.AUTO_RESIZE_LAST_COLUMN);
    fixColumnWidth(postTable, 0, "2009-01-01 17:36:00 ");
    
    // Double-click or enter means "edit selected post".
    ComponentUtilities.bindDoubleClickAndEnter(postTable, editPostAction);
    
    // The delete key means "delete selected post".
    ComponentUtilities.initKeyBinding(postTable, KeyStroke.getKeyStroke(KeyEvent.VK_DELETE, 0), deletePostAction);
    
    final JToolBar toolBar = UiUtils.makeToolBar(newPostAction, editPostAction, deletePostAction);
    
    JComponent ui = new JPanel(new BorderLayout());
    ui.add(toolBar, BorderLayout.NORTH);
    ui.add(new JScrollPane(postTable, JScrollPane.VERTICAL_SCROLLBAR_ALWAYS, JScrollPane.HORIZONTAL_SCROLLBAR_NEVER), BorderLayout.CENTER);
    ui.add(new EStatusBar(), BorderLayout.SOUTH);
    
    setContentPane(ui);
    invalidate();
    validate();
    
    postTable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
    postTable.getSelectionModel().setSelectionInterval(0, 0);
    postTable.requestFocusInWindow();
  }
  
  public void updatePostList() {
    try {
      // FIXME: don't block the EDT!
      postTableModel.setPosts(retrievePostList());
    } catch (Exception ex) {
      // FIXME: we might have logged in okay and experienced trouble collecting the blog list.
      Log.warn("Post retrieval failed.", ex);
      // FIXME: some user-visible indication of what the failure was!
    }
  }
  
  private void initAboutBox() {
    AboutBox aboutBox = AboutBox.getSharedInstance();
    aboutBox.setWebSiteAddress("http://code.google.com/p/blog-baboon/");
    aboutBox.addCopyright("Copyright (C) 2009 Google Inc.");
    aboutBox.addCopyright("All Rights Reserved.");
    aboutBox.setLicense(AboutBox.License.APL_2);
  }
  
  private void initMacOsEventHandlers() {
    if (!GuiUtilities.isMacOs()) {
      return;
    }
    
    Application.getApplication().addApplicationListener(new ApplicationAdapter() {
      @Override public void handleQuit(ApplicationEvent e) {
        e.setHandled(willQuit());
      }
    });
  }
  
  private List<PostEntry> retrievePostList() throws ServiceException, IOException {
    // Request the full unexpurgated feed.
    final Query query = new Query(makeFeedUrl());
    query.setMaxResults(10/*24*/); // FIXME: this isn't necessarily the full feed!
    final BlogPostFeed blogPosts = bloggerService.getFeed(query, BlogPostFeed.class);
    return blogPosts.getEntries();
  }
  
  private List<BlogInfo> retrieveBlogList() throws ServiceException, IOException {
    // Request the metafeed (the feed of feeds, i.e. the list of blogs).
    final URL feedUrl = new URL(METAFEED_URL);
    final BlogFeed resultFeed = bloggerService.getFeed(feedUrl, BlogFeed.class);
    // Convert each BlogEntry into a BlogInfo (which is all we need).
    final ArrayList<BlogInfo> result = new ArrayList<BlogInfo>();
    for (BlogEntry entry : resultFeed.getEntries()) {
      result.add(new BlogInfo(entry));
    }
    Collections.sort(result);
    return result;
  }

  public PostEntry createPost() throws ServiceException, IOException {
    final PostEntry newPost = new PostEntry();
    // You have to set content (or a summary), for some reason.
    // I don't know what a summary is, so content it is...
    newPost.setContent(new HtmlTextConstruct(""));
    newPost.setTitle(new HtmlTextConstruct(""));
    
    newPost.setPubControl(new PubControl());
    newPost.setDraft(true);
    
    // Ask the service to insert the new entry.
    return bloggerService.insert(makeFeedUrl(), newPost);
  }
  
  private URL makeFeedUrl() throws IOException {
    return new URL(FEED_URI_BASE + "/" + chosenBlog.getId() + "/posts/default");
  }
  
  public BlogInfo getBlogInfo() {
    return chosenBlog;
  }
}
